iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0
自我挑戰組

React 開發者的 TypeScript 探索之旅系列 第 21

【 Day 21 】在 TypeScript 中使用 setState 進行狀態管理

  • 分享至 

  • xImage
  •  

本系列文章 GitHub

目前的 Todo List 在輸入欄位為空白時仍然可以送出。今天,我們要來修正這個問題,並為其新增錯誤提示訊息。此外,為了練習我們在 Day08 提到的 Literal Types,我們將在 Todo 成功建立時顯示成功提示訊息。


定義提示訊息型別與狀態

首先,在 components 資料夾中加入檔案 Message.tsx,並建立 Message 元件。Message 元件將放置在 App.tsx 中,並接收來自 Appprops,透過這些 props 來控制顯示與否以及顯示的內容。因此,我們將 type 定義在 App.tsx 並匯出,以便 Message 元件可以共用這個型別。

由於我們的提示訊息只會有「成功」和「失敗」兩種狀態,因此可以使用 Literal Types 來定義 mode 的型別:

export type MessageDetails = {
  visible: boolean
  message: string
  mode: 'error' | 'success'
}

接著,在 App.tsx 中建立狀態,並定義其初始值:

const [MessageDetails, setMessageDetails] = useState<MessageDetails>({
  visible: false,
  message: '',
  mode: 'error',
})

由於 Message 元件會使用 fixed 定位,它可以放置在 <main> 中的任意位置。這裡我們選擇將它放置在頁面底部,並傳入定義好的狀態作為 props

...
  <main className='w-[500px] h-[100dvh] portrait:w-[90%] flex flex-col'>
    <Header image={{ src: logo, alt: 'logo' }}>
      <h1>Todo List</h1>
    </Header>
    <CreateTodo onCreateTodo={createTodoHandler} />
    <TodoList todos={todos} onDeleteTodo={deleteTodoHandler} />
    <Message
      visible={messageDetails.visible}
      mode={messageDetails.mode}
      message={messageDetails.message}
    />
  </main>
...

建立 Message 元件

前往 Message.tsx 並匯入我們在 App.tsx 中定義的 MessageDetails 型別:

import { type MessageDetails } from '../App'

export default function Message({ visible, message, mode }: MessageDetails) {
  return (
    <div
      className={`${mode === 'error' ? 'bg-red-500' : 'bg-green-500'} ${
        visible ? 'flex' : 'hidden'
      } rounded-[5px] p-[10px] fixed bottom-[20px] left-[20px]`}
    >
      <p className='text-[20px]'>{message}</p>
    </div>
  )
}

打開瀏覽器,試著手動更改 App 元件中 messageDetails 的初始值,你應該可以看到像下方截圖中的這樣畫面:

https://ithelp.ithome.com.tw/upload/images/20241001/20169025u6MAcEEJpO.png


更新 Todo 新增邏輯

接著,回到 App 元件,在 createTodoHandler 函式中新增提示訊息的相關邏輯,以處理成功和錯誤狀態:

const createTodoHandler = (title: string) => {
  if (title.trim().length === 0) {
    setMessageDetails({
      visible: true,
      message: 'Input cannot be empty!',
      mode: 'error',
    })
    return
  }
  const newTodo: TodoItem = {
    id: Math.random(),
    title: title,
    isFinished: false,
  }
  setTodos((prevTodos) => [...prevTodos, newTodo])
  setMessageDetails({
    visible: true,
    message: 'Todo created successfully!',
    mode: 'success',
  })
}

處理訊息的顯示與隱藏

目前提示訊息的顯示功能已經完成,接下來,我們要處理提示訊息的自動隱藏。Message 元件的顯示狀態儲存在 App.tsx 中的 messageDetails 狀態,因此,我們需要將更新狀態的 setMessageDetails 函式傳遞給 Message 元件:

<Message
  visible={messageDetails.visible}
  mode={messageDetails.mode}
  message={messageDetails.message}
  onMessageVisible={setMessageDetails}
/>

Message 元件中,我們會使用先前在 Day08 提到的合併型別方法加入 onMessageVisible。為了增強程式碼的可讀性,這裡我們將型別定義獨立提出來,並設置 onMessageVisible 的參數型別為 SetStateAction,泛型參數則使用我們先前定義的 MessageDetails

import { type SetStateAction } from 'react'

type MessageProps = MessageDetails & {
  onMessageVisible: (value: SetStateAction<MessageDetails>) => void
}

最後,加入 useEffect,當 visibletrue 時,觸發計時器,在三秒後自動隱藏提示訊息,並清除上一次的計時器,避免計時器累加造成記憶體的負擔:

useEffect(() => {
  if (visible) {
    const timer = setTimeout(() => {
      onMessageVisible((prev: MessageDetails) => ({
        ...prev,
        visible: false,
      }))
    }, 3000)

    return () => clearTimeout(timer) 
  }
}, [visible])

【 補充 】Literal Types 的彈性應用與選項處理

在最一開始,我們提到了使用 Literal Types 定義型別:

export type MessageDetails = {
  visible: boolean
  message: string
  mode: 'error' | 'success'
}

若有一個元件不需要 mode,而我們在使用 MessageDetails 型別時沒有傳遞 mode 給該元件,那麼 TypeScript 會報錯。在這種情況下,我們可以有兩種解決方法。

第一種寫法是將 mode 設為可選的,並允許其值為 undefined,這樣元件即使沒有傳遞 mode 也不會報錯:

export type MessageDetails = {
  visible: boolean
  message: string
  mode: 'error' | 'success' | undefined
}

第二種寫法是在 mode 後加上 ?,這樣就會讓 mode 成為可選的屬性:

export type MessageDetails = {
  visible: boolean
  message: string
  mode?: 'error' | 'success'
}

兩種寫法效果相同,可以根據個人喜好選擇。


上一篇
【 Day 20 】useRef with TypeScript
下一篇
【 Day 22 】使用 useContext、useReducer 優化資料管理(一)
系列文
React 開發者的 TypeScript 探索之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言